const { NON_TERMINALS } = require('./CompilationEngine.js');
const { TOKEN_TYPE } = require('./JackTokenizer.js');
const { SYMBOL_KIND } = require('./SymbolTable.js');

const SEGMENT = {
  CONSTANT: 'constant',
  ARGUMENT: 'argument',
  LOCAL: 'local',
  STATIC: 'static',
  THIS: 'this',
  THAT: 'that',
  POINTER: 'pointer',
  TEMP: 'temp'
};

const COMMAND = {
  ADD: 'add',
  SUB: 'sub',
  NEG: 'neg',
  EQ: 'eq',
  GT: 'gt',
  LT: 'lt',
  AND: 'and',
  OR: 'or',
  NOT: 'not'
};

const OP_MAP = {
  '+': 'add',
  '*': 'call Math.multiply 2',
  '/': 'call Math.divide 2',
  '-': 'sub',
  '-': 'neg',
  '=': 'eq',
  '>': 'gt',
  '<': 'lt',
  '&': 'and',
  '|': 'or',
  '~': 'not'
};

class VMWriter {
  constructor(classSymbolTable, subroutineSymbolTable) {
    this.parseTree = null;
    this.cache = [];
    this.classSymbolTable = classSymbolTable;
    this.subroutineSymbolTable = subroutineSymbolTable;
    this.className = '';
    this.subroutineName = '';
    this.subroutineType = '';
    this.labelIndex = 0;
  }

  setParseTree(parseTree) {
    this.parseTree = parseTree;
  }

  resetSymbolTable() {
    this.classSymbolTable.reset();
    this.subroutineSymbolTable.reset();
  }

  writeToCache() {
    this.parseTree.forEach((classNode) => this._writeClassToCache(classNode));
    return this.cache.concat('').join('\n');
  }

  _writeClassToCache(classNode) {
    if (classNode.tag !== 'class') return;
    classNode.children.forEach((child) => {
      if (child.tag === TOKEN_TYPE.IDENTIFIER) {
        this.className = child.str;
      } else if (child.tag === NON_TERMINALS.CLASS_VAR_DEC) {
        this._writeClassVarDecToCache(child);
      } else if (child.tag === NON_TERMINALS.SUBROUTINE_DEC) {
        this._writeSubroutineDecToCache(child);
      } // else, ignore
    });
  }

  _writeClassVarDecToCache(classVarDecNode) {
    const children = classVarDecNode.children.slice();
    while (children.length) {
      const node = children.shift();
      if (['static', 'field'].includes(node.str)) {
        const kind = node.str;
        const type = children.shift().str;
        const name = children.shift().str;
        this.classSymbolTable.define(name, type, kind);
        while (children[0].str === ',') {
          children.shift();
          const name = children.shift().str;
          this.classSymbolTable.define(name, type, kind);
        }
      }
    }
  }

  _writeSubroutineDecToCache(subroutineDecNode) {
    const children = subroutineDecNode.children.slice();
    while (children.length) {
      const node = children.shift();
      if (['constructor', 'function', 'method'].includes(node.str)) {
        this.subroutineSymbolTable.reset();
        this.subroutineType = node.str;
        if (this.subroutineType === 'method') {
          this.subroutineSymbolTable.define('this', this.className, SYMBOL_KIND.ARG);
        }
        children.shift();
        this.subroutineName = children.shift().str;
      } else if (node.tag === NON_TERMINALS.PARAMETER_LIST) {
        this._writeParameterListToCache(node);
      } else if (node.tag === NON_TERMINALS.SUBROUTINE_BODY) {
        this._writeSubroutineBodyToCache(node);
      }
    }
  }

  _writeParameterListToCache(parameterListNode) {
    const children = parameterListNode.children.slice();
    while (children.length) {
      const type = children.shift().str;
      const name = children.shift().str;
      this.subroutineSymbolTable.define(name, type, SYMBOL_KIND.ARG);
      while (children[0] && children[0].str === ',') {
        children.shift();
        const type = children.shift().str;
        const name = children.shift().str;
        this.subroutineSymbolTable.define(name, type, SYMBOL_KIND.ARG);
      }
    }
  }

  _writeSubroutineBodyToCache(subroutineBodyNode) {
    let children = subroutineBodyNode.children.slice();
    let nVars = 0;
    while (children.length) {
      while (children[0].tag === NON_TERMINALS.VAR_DEC) {
        nVars += this._writeVarDecToCache(children.shift());
      }
      children.shift();
    }

    this.writeFunction(`${this.className}.${this.subroutineName}`, nVars);
    if (this.subroutineType === 'method') {
      this.writePush('argument', 0);
      this.writePop('pointer', 0);
    } else if (this.subroutineType === 'constructor') {
      const nFields = this.classSymbolTable.varCount(SYMBOL_KIND.FIELD);
      this.writeCall('Memory.alloc', nFields);
      this.writePop('pointer', 0);
    }

    children = subroutineBodyNode.children.slice();
    while (children.length) {
      const node = children.shift();
      if (node.tag === NON_TERMINALS.STATEMENTS) {
        this._writeStatementsToCache(node);
      }
    }
  }

  _writeVarDecToCache(varDecNode) {
    const children = varDecNode.children.slice();
    children.shift();
    let nVars = 0;
    const type = children.shift().str;
    const name = children.shift().str;
    this.subroutineSymbolTable.define(name, type, SYMBOL_KIND.VAR);
    nVars++;
    while (children[0].str === ',') {
      children.shift();
      this.subroutineSymbolTable.define(children.shift().str, type, SYMBOL_KIND.VAR);
      nVars++;
    }
    return nVars;
  }

  _writeStatementsToCache(statementsNode) {
    const children = statementsNode.children.slice();
    while (children.length) {
      const node = children.shift();
      if (node.tag === NON_TERMINALS.IF) {
        this._writeIfStatementToCache(node);
      } else if (node.tag === NON_TERMINALS.DO) {
        this._writeDoStatementToCache(node);
      } else if (node.tag === NON_TERMINALS.WHILE) {
        this._writeWhileStatementToCache(node);
      } else if (node.tag === NON_TERMINALS.RETURN) {
        this._writeReturnStatementToCache(node);
      } else if (node.tag === NON_TERMINALS.LET) {
        this._writeLetStatementToCache(node);
      }
    }
  }

  _writeLetStatementToCache(letStatementNode) {
    const children = letStatementNode.children.slice();
    let varName;
    while (children.length) {
      const node = children.shift();
      if (node.tag === TOKEN_TYPE.IDENTIFIER) {
        varName = node.str;
        if (children[0] && children[0].str === '[') {
          const [left, exp1, right] = children.slice(0, 3);
          const { seg, index } = this._getSegAndIndexInSymbolTable(varName);
          this.writePush(seg, index);
          this._writeExpressionToCache(exp1);
          this.writeArithmetic('add');
          children.shift();
          children.shift();
          children.shift();
          const [eq, exp2, semi] = children;
          this._writeExpressionToCache(exp2);
          this.writePop('temp', 0);
          this.writePop('pointer', 1);
          this.writePush('temp', 0);
          this.writePop('that', 0);
        } else {
          const [eq, exp, semi] = children;
          this._writeExpressionToCache(exp);
          const { seg, index } = this._getSegAndIndexInSymbolTable(varName);
          this.writePop(seg, index);
        }
      }
    }
  }

  _writeIfStatementToCache(ifStatementNode) {
    const children = ifStatementNode.children.slice();
    const ifLabel = 'L' + this.labelIndex++;
    const elseLabel = 'L' + this.labelIndex++;
    while (children.length) {
      const node = children.shift();
      if (node.tag === TOKEN_TYPE.SYMBOL) continue;

      if (node.tag === NON_TERMINALS.EXPRESSION) {
        this._writeExpressionToCache(node);
        this.writeArithmetic('not');
        this.writeIf(elseLabel);
        children.shift();
        children.shift();
        this._writeStatementsToCache(children.shift());
        this.writeGoto(ifLabel);
        this.writeLabel(elseLabel);
        continue;
      }
      if (node.str === 'else') {
        children.shift();
        this._writeStatementsToCache(children.shift());
      }
    }
    this.writeLabel(ifLabel);
  }

  _writeWhileStatementToCache(whileStatementNode) {
    const startLabel = 'L' + this.labelIndex++;
    const endLabel = 'L' + this.labelIndex++;
    this.writeLabel(startLabel);
    const children = whileStatementNode.children.slice();
    while (children.length) {
      const node = children.shift();
      if (node.tag === NON_TERMINALS.EXPRESSION) {
        this._writeExpressionToCache(node);
        this.writeArithmetic('not');
        this.writeIf(endLabel);
      }
      if (node.tag === NON_TERMINALS.STATEMENTS) {
        this._writeStatementsToCache(node);
        this.writeGoto(startLabel);
      }
    }
    this.writeLabel(endLabel);
  }

  _writeDoStatementToCache(doStatementNode) {
    const children = doStatementNode.children.slice();
    children.shift();
    children.pop();
    this._writeTermToCache({ children });
    this.writePop('temp', 0);
  }

  _writeReturnStatementToCache(returnStatementNode) {
    const expression = returnStatementNode.children.find((item) => item.tag === NON_TERMINALS.EXPRESSION);
    if (expression) {
      this._writeExpressionToCache(expression);
    } else {
      this.writePush('constant', 0);
    }
    this.writeReturn();
  }

  _writeExpressionToCache(expressionNode) {
    const children = expressionNode.children.slice();
    const [term1, op, term2] = children;
    if (!term2) {
      this._writeTermToCache(term1);
    } else {
      this._writeTermToCache(term1);
      this._writeTermToCache(term2);
      this.writeArithmetic(OP_MAP[op.str]); // no escape
    }
  }

  _writeTermToCache(termNode) {
    const children = termNode.children.slice();
    const node = children[0];
    if (children.length === 1) {
      if (node.tag === TOKEN_TYPE.INT_CONST) {
        this.writePush('constant', node.str);
      } else if (node.str === 'null' || node.str === 'false') {
        this.writePush('constant', 0);
      } else if (node.str === 'true') {
        this.writePush('constant', 1);
        this.writeArithmetic('neg');
      } else if (node.str === 'this') {
        this.writePush('pointer', 0);
      } else if (node.tag === TOKEN_TYPE.IDENTIFIER) {
        const varName = node.str;
        const symbolResult = this._getSegAndIndexInSymbolTable(varName);
        this.writePush(symbolResult.seg, symbolResult.index);
      }
    } else if (children.length === 2) {
      const [op, term] = children;
      this._writeTermToCache(term);
      const arith = op.str === '-' ? 'neg' : 'not';
      this.writeArithmetic(arith);
    } else if (children.length === 3) {
      const [openParen, exp, closeParen] = children;
      this._writeExpressionToCache(exp);
    } else if (node.tag === TOKEN_TYPE.IDENTIFIER) {
      if (children.length === 6) {
        const [varOrClassName, dot, subroutineName, openParen, expList, closeParen] = children;
        const symbolResult = this._getSegAndIndexInSymbolTable(varOrClassName.str);
        if (symbolResult) {
          this.writePush(symbolResult.seg, symbolResult.index);
          this._writeExpressionListToCache(expList);
          const nArgs = expList.children.filter((item) => item.str !== ',').length;
          const className =
            this.subroutineSymbolTable.typeOf(subroutineName.str) || this.classSymbolTable.typeOf(subroutineName.name);
          this.writeCall(`${className}.${subroutineName.str}`, nArgs + 1);
        } else {
          this._writeExpressionListToCache(expList);
          const nArgs = expList.children.filter((item) => item.str !== ',').length;
          const className = varOrClassName.str;
          this.writeCall(`${className}.${subroutineName.str}`, nArgs);
        }
      } else if (children.length === 4) {
        if (children[1].str === '[') {
          const [varName, tempOpen, exp, tempClose] = children;
          const { seg, index } = this._getSegAndIndexInSymbolTable(varName);
          this.writePush(seg, index);
          this._writeExpressionToCache(exp);
          this.writeArithmetic('add');
        } else if (children[1].str === '(') {
          const [subroutineName, openParen, expList, closeParen] = children;
          this.writePush('this', 0);
          this._writeExpressionListToCache(expList);
          const nArgs = expList.children.filter((item) => item.str !== ',').length;
          const className =
            this.subroutineSymbolTable.typeOf(subroutineName) || this.classSymbolTable.typeOf(subroutineName);
          this.writeCall(`${className}.${subroutineName}`, nArgs + 1);
        } else {
          this._syntaxError(node);
        }
      }
    } else {
      this._syntaxError(node);
    }
  }

  _writeExpressionListToCache(expListNode) {
    const children = expListNode.children.slice();
    if (children.length) {
      const node = children.shift();
      this._writeExpressionToCache(node);
      while (children[0] && children[0].str === ',') {
        children.shift();
        this._writeExpressionToCache(children.shift());
      }
    }
  }

  _getSegAndIndexInSymbolTable(varOrClassName) {
    if (this.subroutineSymbolTable.kindOf(varOrClassName)) {
      const kind = this.subroutineSymbolTable.kindOf(varOrClassName);
      const index = this.subroutineSymbolTable.indexOf(varOrClassName);
      const seg = kind === SYMBOL_KIND.ARG ? 'argument' : 'local';
      return { seg, index };
    } else if (this.classSymbolTable.kindOf(varOrClassName)) {
      const kind = this.classSymbolTable.kindOf(varOrClassName);
      const index = this.classSymbolTable.indexOf(varOrClassName);
      const seg = kind === SYMBOL_KIND.STATIC ? 'static' : 'this';
      return { seg, index };
    } else {
      return null;
    }
  }

  /**
   * @param {SEGMENT} segment
   * @param {number} index
   */
  writePush(segment, index) {
    this.cache.push(`push ${segment} ${index}`);
  }

  /**
   * @param {SEGMENT} segment
   * @param {number} index
   */
  writePop(segment, index) {
    this.cache.push(`pop ${segment} ${index}`);
  }

  /**
   * @param {COMMAND} command
   */
  writeArithmetic(command) {
    this.cache.push(command);
  }

  /**
   * @param {string} label
   */
  writeLabel(label) {
    this.cache.push(`label ${label}`);
  }

  /**
   * @param {string} label
   */
  writeGoto(label) {
    this.cache.push(`goto ${label}`);
  }

  /**
   * @param {string} label
   */
  writeIf(label) {
    this.cache.push(`if-goto ${label}`);
  }

  /**
   * @param {string} name
   * @param {number} nArgs
   */
  writeCall(name, nArgs) {
    this.cache.push(`call ${name} ${nArgs}`);
  }

  /**
   * @param {string} name
   * @param {number} nVars
   */
  writeFunction(name, nVars) {
    this.cache.push(`function ${name} ${nVars}`);
  }

  writeReturn() {
    this.cache.push('return');
  }

  _syntaxError(node) {
    throw new Error('哦豁, 不能解析', node);
  }

  close() {}
}

module.exports.VMWriter = VMWriter;
